package com.zzyl.nursing.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zzyl.common.constant.CacheConstants;
import com.zzyl.common.exception.base.BaseException;
import com.zzyl.nursing.domain.AlertData;
import com.zzyl.nursing.domain.AlertRule;
import com.zzyl.nursing.domain.DeviceData;
import com.zzyl.nursing.mapper.AlertRuleMapper;
import com.zzyl.nursing.mapper.DeviceMapper;
import com.zzyl.nursing.service.IAlertDataService;
import com.zzyl.nursing.service.IAlertRuleService;
import com.zzyl.system.mapper.SysUserRoleMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 报警规则Service业务层处理
 *
 * @author mr.luo
 * @date 2025-05-29
 */
@Slf4j
@Service
public class AlertRuleServiceImpl extends ServiceImpl<AlertRuleMapper, AlertRule> implements IAlertRuleService {
    @Autowired
    private AlertRuleMapper alertRuleMapper;

    /**
     * 查询报警规则
     *
     * @param id 报警规则主键
     * @return 报警规则
     */
    @Override
    public AlertRule selectAlertRuleById(Long id) {
        return getById(id);
    }

    /**
     * 查询报警规则列表
     *
     * @param alertRule 报警规则
     * @return 报警规则
     */
    @Override
    public List<AlertRule> selectAlertRuleList(AlertRule alertRule) {
        return alertRuleMapper.selectAlertRuleList(alertRule);
    }

    /**
     * 新增报警规则
     *
     * @param alertRule 报警规则
     * @return 结果
     */
    @Override
    public int insertAlertRule(AlertRule alertRule) {
        return save(alertRule) ? 1 : 0;
    }

    /**
     * 修改报警规则
     *
     * @param alertRule 报警规则
     * @return 结果
     */
    @Override
    public int updateAlertRule(AlertRule alertRule) {
        return updateById(alertRule) ? 1 : 0;
    }

    /**
     * 批量删除报警规则
     *
     * @param ids 需要删除的报警规则主键
     * @return 结果
     */
    @Override
    public int deleteAlertRuleByIds(Long[] ids) {
        return removeByIds(Arrays.asList(ids)) ? 1 : 0;
    }

    /**
     * 删除报警规则信息
     *
     * @param id 报警规则主键
     * @return 结果
     */
    @Override
    public int deleteAlertRuleById(Long id) {
        return removeById(id) ? 1 : 0;
    }

    @Value("{SysRole.admin}") //管理员
    private String userAdmin;
    @Value("{SysRole.Caregivers}") //护理员
    private String caregivers;
    @Value("{SysRole.MaintenanceWorker}") // 修理工
    private String maintenanceWorker;
    @Autowired
    private DeviceMapper deviceMapper;  //根据设备id查询护理员id
    @Autowired
    private SysUserRoleMapper sysUserRoleMapper;  //根据角色id查询用户id
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private IAlertDataService alertDataService;

    /**
     * 设备报警处理
     */
    @Override
    public void deviceAlertFilter() {
        log.info("[报警规则]：定时任务开始处理");
        //首先先查询所有status启用的规则
        Long count = this.lambdaQuery().eq(AlertRule::getStatus, 1).count();
        //判断规则是否为空
        if (count <= 0) {
            log.error("[报警规则]：没有报警规则不需要进行处理");
            return;
        }
        //报警规则不为空，查询所有的redis的上报数据
        //存的时候存的是hash类型的，所以可以直接使用hash，values方法获取所有的值
        List<Object> values = redisTemplate.opsForHash().values(CacheConstants.DEVICE_LAST_DATA_KEY);
        //对获取的数据进行校验
        if (CollectionUtils.isEmpty(values)) {
            log.error("[报警规则]：没有获取到设备的上报数据，无需进行处理");
            return;
        }
        List<DeviceData> deviceDataList = new ArrayList<>();
        //数据非空，进行数据格式化，存的时候是deviceData 数据类型的,所以将其还原成DeviceData.class类型
        //由于拿到的可能是多个设备的的values，每一个设备的values就是一个集合，所以需要进行遍历，然后再对每一个设备进行转换成集合
        values.forEach(s -> {
            List<DeviceData> list = JSONUtil.toList((String) s, DeviceData.class);
            //将所有的数据都放到一个集合中，方便后面的操作
            //使用集合的方法，将另外的一个相同类型的集合中的所有数据都加到另外的一个集合中
            deviceDataList.addAll(list);
        });
        if (CollectionUtils.isEmpty(deviceDataList)) {
            log.error("[报警规则]：没有获取到设备的上报数据，无需进行处理");
            return;
        }

        //拿到所有的设备数据，进行分析数据，做报警的筛选与处理
        //遍历所有设备上报的数据，进行分析处理
        for (DeviceData deviceData : deviceDataList) {
            //如果上报的数据上报的时间太久远，就不需要进行处理了
            //系统业务规则要求：当数据上报的时间距离当前时间超过十分钟，就不在进行处理
            int expiredTime = 600;
            //获取数据上报时间
            LocalDateTime alarmTime = deviceData.getAlarmTime();
            //使用LocalDateTimeUtil进行判断是否超时：上报时间，现在时间，时间单位
            long between = LocalDateTimeUtil.between(alarmTime, LocalDateTime.now(), ChronoUnit.SECONDS);
            if (between > expiredTime) {
                log.error("[报警规则]:{},{},设备上报时间至校验规则时间距离超过十分钟，不予以处理",
                        deviceData.getDeviceName(), deviceData.getProductName());
                continue;
            }
            //TODO 查看当前上报数据对应的产品类型下所有相同功能绑定的全部的规则数据
            List<AlertRule> alertAllRuleList = this.lambdaQuery()
                    .eq(AlertRule::getProductName, deviceData.getProductName())
                    .eq(AlertRule::getFunctionId, deviceData.getFunctionId())
                    .eq(AlertRule::getProductKey, deviceData.getProductKey())
                    .eq(AlertRule::getIotId, -1)
                    .eq(AlertRule::getStatus, 1)
                    .list();
            if (CollectionUtils.isEmpty(alertAllRuleList)) {
                alertAllRuleList = new ArrayList<>();
            }
            //TODO 查询当前上报数据对应的设备的规则
            List<AlertRule> alertRules = this.lambdaQuery()
                    .eq(AlertRule::getProductName, deviceData.getProductName())
                    .eq(AlertRule::getFunctionId, deviceData.getFunctionId())
                    .eq(AlertRule::getProductKey, deviceData.getProductKey())
                    .eq(AlertRule::getIotId, deviceData.getIotId())
                    .eq(AlertRule::getStatus, 1)
                    .list();
            if (CollectionUtils.isEmpty(alertRules)) {
                alertRules = new ArrayList<>();
            }
            //合并所有的报警规则
            alertAllRuleList.addAll(alertRules);
            if (CollectionUtils.isEmpty(alertAllRuleList)) {
                log.error("[报警规则]:当前上报的数据没有该设备的规则");
                continue;
            }
            //去重，去除重复的数据，使用工具类Collutil
            ArrayList<AlertRule> distinct = CollUtil.distinct(alertAllRuleList);
            //遍历每一条规则
            for (AlertRule alertRule : distinct) {
                //判断是否在生效时间内,报警生效时间段 时间格式是DateTime类型
                String alertEffectivePeriod = alertRule.getAlertEffectivePeriod(); //格式是：00:00:00~23:59:59
                //然后从 ~ 分割
                String[] split = alertEffectivePeriod.split("~");
                LocalTime startTime = LocalTime.parse(split[0]);//开始时间
                LocalTime endTime = LocalTime.parse(split[1]);//结束时间
                //判断上报时间是不是在这个时间段里面,设备的上传的时间是LocalDateTime类型的所以需要转化成DateTime类型的
                LocalTime localTime = alarmTime.toLocalTime();
                if (localTime.isBefore(startTime) || localTime.isAfter(endTime)) {
                    log.error("[报警规则]:上报时间没有在校验规则的时间范围内，无需处理");
                    continue;
                }
                //检验数据是否达到阈值
                //查看设备上报的数值，是不是超过了阈值
                Double dataValue = Double.valueOf(deviceData.getDataValue());
                //TODO 规则设置的阈值  （多个规则之间阈值的规则优先级） 已解决
                Double value = alertRule.getValue();
                //获取操作的运算符
                String operator = alertRule.getOperator();
                //通过工具类NumberUtil，判断设备上报数据与设置的阈值之间的大小
                //compare 等于0相等   等于-1 是小于  等于1是大于
                int compare = NumberUtil.compare(dataValue, value);
                if (operator.equals(">=") && compare >= 0 || operator.equals("<") && compare < 0) {
                    //达到阈值
                    //到了第三层的校验，查看redis中是否有沉默周期，如果有就直接结束
                    //使用CharSequenceUtil工具类，往设置的常量key中{}：{}：{}中填充数据
                    String silentKey = CharSequenceUtil.format(CacheConstants.IOT_ALERT_SILENT_PREFIX,
                            deviceData.getIotId(), deviceData.getFunctionId(), alertRule.getId());
                    //像redis中取沉默值
                    String s = redisTemplate.opsForValue().get(silentKey);
                    //判断s是否有值，如果有值说明还在沉默期间，就不处理
                    if (StrUtil.isNotBlank(s)) {
                        log.error("[报警规则]:沉默周期还没有结束，无需进行处理");
                        continue;
                    } else {
                        //没有沉默值了，进入第四层校验
                        //从redis中查询已经报警的次数（持续周期），只有达到设置的报警的次数（持续周期），才可以继续进行
                        String countKey = CharSequenceUtil.format(CacheConstants.IOT_ALERT_COUNT_PREFIX, deviceData.getIotId(),
                                deviceData.getFunctionId(), alertRule.getId());
                        //对持续周期进行增加：加上本次的次数，进行校验，自增1，看是否达到报警的次数
                        //对于redis中没有的，会这个自增会自动创建一个kv ，没有设置步增，默认从1开始，每次加1 返回的是增加之后的值
                        Long increment = redisTemplate.opsForValue().increment(countKey);
                        if (increment < alertRule.getDuration()) {
                            //说明还没有达到设置的报警周期的次数，不用处理
                            log.error("[报警规则]:未达到报警的持续周期次数，无需进行处理");
                            continue;
                        }
                        //现在是属于满足所有的筛选条件准备进行插入数据的操作
                        //首先准备保存数据之前，需要保存沉默周期，
                        //TODO 保存沉默周期的值到Redis，过期时间为沉默周期对应的秒数  "123"只是为了校验这个沉默是否存在，没有实际意义
                        //setIfAbsent 当键不存在的时候才会设置
                        redisTemplate.opsForValue().setIfAbsent(silentKey, "123",
                                alertRule.getAlertSilentPeriod() * 60, TimeUnit.SECONDS);
                        //同时需要删除持续周期，持续周期要从头开始
                        redisTemplate.delete(countKey);


                        //查询老人的护理人员
                        List<Long> nursingIds = null;
                        //这时候需要查询这个报警规则是对应的老人的还是设备的
                        if (alertRule.getAlertDataType() == 0) {
                            String iotId = deviceData.getIotId();
                            //是老人的异常数据
                            //是老人的异常数据就需要根据随身设备或者固定设备查询老人的护理人员
                            if (deviceData.getLocationType() == 0) {
                                //随身设备
                                nursingIds = deviceMapper.selectNursingIdsByIotIdWithElder(iotId);
                            } else {
                                //固定设备
                                nursingIds = deviceMapper.selectNursingIdsByIotIdWithBed(iotId);
                            }
                        } else {
                            //是设备的异常数据
                            //需要根据角色的名字查询维修工 maintenanceWorker
                            nursingIds = sysUserRoleMapper.selectUserIdByRoleName(maintenanceWorker);
                        }
                        //查询超级管理员 //管理员也有可能是多个
                        List<Long> adminIds = sysUserRoleMapper.selectUserIdByRoleName(userAdmin);
                        //合并所有的用户id
                        nursingIds.addAll(adminIds);
                        //由于用户与角色是多对多的关系，说明一个角色可能对应多个用户，所以可能涉及到重复的用户，所以需要去重
                        ArrayList<Long> nursingIdList = CollUtil.distinct(nursingIds);
                        //终于开始封装对象了，先封装一些基础的对象的数据
                        AlertData alertData = new AlertData();
                        BeanUtils.copyProperties(deviceData, alertData);
                        //注意属性拷贝之后，一定要对主键id进行设置成null，因为新增主键是自增的
                        alertData.setId(null);
                        //对一些拷贝不了数据进行单独封装
                        alertData.setType(alertRule.getAlertDataType());//设备数据类型 0-老人异常数据 1-设备异常数据
                        alertData.setAlertRuleId(alertRule.getId());//规则id
                        alertData.setStatus(0);//0-待处理  1-已处理
                        //报警原因，格式：功能名称+运算符+阈值+持续周期+聚合周期
                        //注意设置报警原因，有模板还需要用到CharSequenceUtil工具类format方法进行组装
                        String reasonTemplate = "功能{}{}{}，持续了{}个周期才报警，沉默周期：{}";
                        String alertReason = CharSequenceUtil.format(reasonTemplate,
                                alertRule.getFunctionName(),
                                alertRule.getOperator(),
                                alertRule.getValue(),
                                alertRule.getDuration(),
                                alertRule.getAlertSilentPeriod()
                        );
                        alertData.setAlertReason(alertReason);//报警原因

                        //所有用户报警数据的集合
                        List<AlertData> alertDataList = new ArrayList<>();
                        for (Long userId : nursingIdList) {
                            //封装用户报警数据，设置用户ID
                            AlertData userAlertData = new AlertData();
                            BeanUtils.copyProperties(alertData, userAlertData);
                            userAlertData.setUserId(userId);
                            alertDataList.add(userAlertData);
                        }
                        //批量保存所有用户报警数据
                        boolean result = alertDataService.saveBatch(alertDataList);
                        if (!result) {
                            throw new BaseException("保存用户保存数据失败");
                        }
                    }
                } else {
                    //未达到阈值
                    log.error("[定时报警规则校验]数据上报的值没有达到规则阈值，无需处理");
                    continue;
                }

            }
        }
    }
}
